package battlecruiser.ship.placement.fleet;

import battlecruiser.board.Board;
import battlecruiser.board.ObservableBoard;
import battlecruiser.board.cell.Cell;
import battlecruiser.board.cell.CellStatus;
import battlecruiser.board.cell.coordinate.Coordinate;
import battlecruiser.ship.placement.rules.BasicShipPlacingRules;
import battlecruiser.ship.placement.rules.ShipPlacingRules;
import battlecruiser.ship.placement.ship.Ship;
import battlecruiser.ship.placement.ship.ShipDirection;
import battlecruiser.ship.placement.ship.ShipType;

import java.util.*;

/**
 * @author tymoshya
 * @since 20.06.12
 */
public class FleetFactory {

    private static ShipPlacingRules _shipPlacingRules = new BasicShipPlacingRules();

    public static Fleet createEmptyFleet() {
        Fleet result = new Fleet();
        result.setShips(new LinkedHashSet<Ship>());
        Map<ShipType, Integer> shipTypesPlaced = new TreeMap<ShipType, Integer>();
        for (Map.Entry<ShipType, Integer> rule : _shipPlacingRules.getRules().entrySet()) {
            shipTypesPlaced.put(rule.getKey(), 0);
        }
        result.setShipTypesPlaced(shipTypesPlaced);
        result.setShipPlacingRules(_shipPlacingRules.getRules());
        return result;
    }

    public static Fleet createFleetFromGameBoard(ObservableBoard observableBoard) {
        ShipSquadron shipSquadron = locateShips(observableBoard);
        Fleet result = new Fleet(shipSquadron.getShips(), shipSquadron.getShipTypesPlaced(), _shipPlacingRules.getRules());
        result.markReady();
        return result;
    }

    private static ShipSquadron locateShips(ObservableBoard observableBoard) {
        ShipSquadron result = new ShipSquadron();

        // get all ship coordinates from the observableBoard to a CoordinatesSet
        List<Coordinate> shipCoordinates = locateShipCoordinates(observableBoard);

        // checks if coordinate is ship recursively
        result.addSquadron(parseCoordinates(result, shipCoordinates, observableBoard));
        return result;
    }

    private static List<Coordinate> locateShipCoordinates(ObservableBoard observableBoard) {
        List<Coordinate> result = new ArrayList<Coordinate>();
        Cell[][] cells = observableBoard.getCells();
        for (int i = 0; i < Board.getHeight(); i++) {
            for (int j = 0; j < Board.getWidth(); j++) {
                if (cells[i][j].getStatus() == CellStatus.SHIP) {
                    result.add(cells[i][j].getCoordinate());
                }
            }
        }
        return result;
    }

    private static ShipSquadron parseCoordinates(ShipSquadron shipSquadron, List<Coordinate> coordinates, ObservableBoard observableBoard) {
        /*
            1. If coordinates is not empty -> Take first coordinate the list
            2. If it is OneDeckShip:
                2.1. Add it to Squadron
                2.2. Remove this coordinate from the list
                2.3. invoke parseCoordinates with updated Squadron and reduced coordinateSet
            3. If it is MultiDeckShip:
                3.1. Count decks
                3.2. Add it to Squadron
                3.3. Remove all ship's coordinates from the list
                3.4. Invoke on reduced
         */
        if (!coordinates.isEmpty()) {
            Coordinate firstDeckCoordinate = coordinates.get(0);
            List<Coordinate> shipCoordinates = findShipCoordinates(firstDeckCoordinate, observableBoard);
            shipSquadron.addShip(shipCoordinates);
            coordinates.removeAll(shipCoordinates);
            shipSquadron.addSquadron(parseCoordinates(shipSquadron, coordinates, observableBoard));
        }
        return shipSquadron;
    }

    private static List<Coordinate> findShipCoordinates(Coordinate firstDeckCoordinate, ObservableBoard observableBoard) {
        List<Coordinate> coordinates = new ArrayList<Coordinate>(Arrays.asList(firstDeckCoordinate));
        if (hasNeighbour(firstDeckCoordinate, ShipDirection.VERTICAL, observableBoard)) {
            coordinates.addAll(findNeighbours(firstDeckCoordinate, ShipDirection.VERTICAL, observableBoard));
        } else if (hasNeighbour(firstDeckCoordinate, ShipDirection.HORIZONTAL, observableBoard)) {
            coordinates.addAll(findNeighbours(firstDeckCoordinate, ShipDirection.HORIZONTAL, observableBoard));
        }
        return coordinates;
    }

    private static boolean hasNeighbour(Coordinate coordinate, ShipDirection shipDirection, ObservableBoard observableBoard) {
        Coordinate next = nextCoordinate(coordinate, shipDirection);
        return ObservableBoard.isWithinTheBoard(next) && (observableBoard.getCells()[next.getI()][next.getJ()].getStatus() == CellStatus.SHIP);
    }

    private static List<Coordinate> findNeighbours(Coordinate firstDeckCoordinate, ShipDirection shipDirection, ObservableBoard observableBoard) {
        List<Coordinate> result = new ArrayList<Coordinate>();
        Coordinate next = nextCoordinate(firstDeckCoordinate, shipDirection);
        result.add(next);
        while (hasNeighbour(next, shipDirection, observableBoard)) {
            next = nextCoordinate(next, shipDirection);
            result.add(next);
        }
        return result;
    }

    private static Coordinate nextCoordinate(Coordinate coordinate, ShipDirection shipDirection) {
        int nextI, nextJ;
        if (shipDirection == ShipDirection.VERTICAL) {
            nextI = coordinate.getI() + 1;
            nextJ = coordinate.getJ();
        } else {
            nextI = coordinate.getI();
            nextJ = coordinate.getJ() + 1;
        }
        return new Coordinate(nextI, nextJ);
    }

    private static class ShipSquadron {
        private Set<Ship> ships;
        private Map<ShipType, Integer> shipTypesPlaced;

        private ShipSquadron() {
            ships = new LinkedHashSet<Ship>();
            shipTypesPlaced = new TreeMap<ShipType, Integer>();
            for (ShipType shipType : _shipPlacingRules.getRules().keySet()) {
                shipTypesPlaced.put(shipType, 0);
            }
        }

        public void addShip(List<Coordinate> coordinates) {
            ShipType shipType = ShipType.fromInteger(coordinates.size());
            ShipDirection shipDirection = arePartsOfVerticalShip(coordinates) ? ShipDirection.VERTICAL : ShipDirection.HORIZONTAL;
            Coordinate firstDeckCoordinate = coordinates.get(0);
            Ship ship = new Ship(shipType, shipDirection, firstDeckCoordinate);
            ships.add(ship);
            shipTypesPlaced.put(shipType, shipTypesPlaced.get(shipType) + 1);
        }

        public void addSquadron(ShipSquadron shipSquadronToAdd) {
            ships.addAll(shipSquadronToAdd.getShips());
            shipTypesPlaced.putAll(shipSquadronToAdd.getShipTypesPlaced());
        }

        public Set<Ship> getShips() {
            return ships;
        }

        public Map<ShipType, Integer> getShipTypesPlaced() {
            return shipTypesPlaced;
        }

        private boolean arePartsOfVerticalShip(List<Coordinate> coordinates) {
            boolean result;
            if (coordinates.size() == 1) {
                result = true;                              // Convention: 1-Deck ships are considered vertical
            } else {
                Coordinate first = coordinates.get(0);
                Coordinate second = coordinates.get(1);
                result = first.getJ() == second.getJ();
            }
            return result;
        }
    }
}
